/**
 * @author Daniel Monteiro
 * */

package br.odb.bzk3.android;

import java.io.IOException;
import java.util.ArrayList;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.res.Resources.NotFoundException;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import android.util.Log;
import br.odb.libscene.Actor;
import br.odb.libscene.Constants;
import br.odb.libscene.Mesh;
import br.odb.libscene.Sector;
import br.odb.libscene.Sector.InvalidSlotException;
import br.odb.libstrip.VertexArrayManager;
import br.odb.utils.Utils;
import br.odb.utils.math.Vec3;
import bzk3.geometry.GLES1Triangle;
import bzk3.geometry.GLESIndexedSetFace;
import bzk3.geometry.GLESVertexArrayManager;
import bzk3.geometry.GLES1Square;
import bzk3.geometry.GLES1SquareFactory;
import bzk3.geometry.ObjMesh;

public class GLESRenderer implements GLSurfaceView.Renderer {

	final private ArrayList<GameActor> actors;
	private float angle;
	int[] lights = new int[96];
	private int back;
	private GLESVertexArrayManager fixedGeometryManager;
	public GameActor cameraActor;
	private int direction;
	float parcel;
	private String displayList;
	private long frame;
	final GLESVertexArrayManager manager = new GLESVertexArrayManager();
	final private ArrayList<GLESIndexedSetFace> sceneGeometryToRender;
	final public ArrayList<GLESIndexedSetFace> screenShapesToRender;
	private int top;
	int visiblePolys;
	private GameSector[] toVisit;
	// private int[] incidingVisit;
	public static final Vec3[] relativePositions = {
			new Vec3(0.0f, 0.0f, -1.0f), new Vec3(1.0f, 0.0f, 0.0f),
			new Vec3(0.0f, 0.0f, 1.0f), new Vec3(-1.0f, 0.0f, 0.0f),
			new Vec3(0.0f, -1.0f, 0.0f), new Vec3(0.0f, 1.0f, 0.0f) };

	private GameWorld world;
	final private ArrayList<GameActor> lightEmissors;
	private boolean shouldResetView = true;
	private ArrayList<Mesh> meshes = new ArrayList<Mesh>();
	public Vec3 accel;
	// ------------------------------------------------------------------------------------------------------------
	public GLESRenderer() {
		super();

		lightEmissors = new ArrayList<GameActor>();
		sceneGeometryToRender = new ArrayList<GLESIndexedSetFace>();
		actors = new ArrayList<GameActor>();
		screenShapesToRender = new ArrayList<GLESIndexedSetFace>();
		accel = new Vec3();
	}

	// ------------------------------------------------------------------------------------------------------------

	public void addGeometryToScene(GLESIndexedSetFace isf) {
		sceneGeometryToRender.add(isf);
	}

	public void addGeometryToScreen(GLESIndexedSetFace s) {
		screenShapesToRender.add(s);

	}

	public void addToScene(GameActor actor) {
		lightEmissors.add(actor);
		actors.add(actor);
	}

	public void addToScene(GameSector sector) {
		GLES1Square square;

		for (int d = 0; d < 6; d++)
			try {
				if ((sector.getLink(d) == Constants.NO_LINK || sector
						.getDoor(d) != null) && sector.meshWalls[d] == null) {

					square = null;

					switch (d) {
					case 0:
						square = GLES1SquareFactory.MakeXY(sector.getX0(),
								sector.getY0(), sector.getX1(), sector.getY1(),
								sector.getZ0(), 0, false);
						break;
					case 2:
						square = GLES1SquareFactory.MakeXY(sector.getX0(),
								sector.getY0(), sector.getX1(), sector.getY1(),
								sector.getZ1(), 0, false);
						break;
					case 1:
						square = GLES1SquareFactory.MakeYZ(sector.getY0(),
								sector.getZ0(), sector.getY1(), sector.getZ1(),
								sector.getX1(), 0, false);
						break;
					case 3:
						square = GLES1SquareFactory.MakeYZ(sector.getY0(),
								sector.getZ0(), sector.getY1(), sector.getZ1(),
								sector.getX0(), 0, false);
						break;
					case 4:
						square = GLES1SquareFactory.MakeXZ(sector.getX0(),
								sector.getZ0(), sector.getX1(), sector.getZ1(),
								sector.getY0(), 0, false);
						break;
					case 5:
						square = GLES1SquareFactory.MakeXZ(sector.getX0(),
								sector.getZ0(), sector.getX1(), sector.getZ1(),
								sector.getY1(), 0, false);
						break;
					}

					if (square == null)
						continue;

					square.setColor(sector.getColor(d).getR() / 255.0f, sector
							.getColor(d).getG() / 255.0f, sector.getColor(d)
							.getB() / 255.0f, 0.5f);
					visiblePolys++;
					sector.face[d] = square;
					square.setVisibility(false);

					if (sector.getDoor(d) != null
							&& sector.getDoor(d).getMesh() == null) {

						Mesh mesh = new Mesh();
						mesh.addFace(square);
						sector.getDoor(d).setMesh(mesh);
						sector.getDoor(d).close();
					}

					// this.sceneGeometryToRender.add(square);
					// manager.pushIntoFrameAsStatic(
					// square.getVertexData(), square.getColorData() );
				}

			} catch (InvalidSlotException e) {
				e.printStackTrace();
			}
	}

	// ------------------------------------------------------------------------------------------------------------

	public void calculateLightning() {
		GameSector sec;
		int size = lightEmissors.size();

		for (int c = 0; c < size; c++)
			if (lightEmissors.get(c).candelas > 0)
				lit(lightEmissors.get(c).getCurrentSector(),
						lightEmissors.get(c).candelas);
	}

	public void resetLightning() {
		int size = this.world.getTotalSectors();
		for (int c = 0; c < size; c++) {
			((GameSector) this.world.getSector(c)).reset();
		}
	}

	// -----------

	/**
	 * @return the world
	 */
	public GameWorld getWorld() {
		return world;
	}

	public void initBuffers(int numSectors) {
		toVisit = new GameSector[numSectors];
		manager.init(visiblePolys);
		manager.flushToGLES();
		GameSector sector;

		GLES1Triangle[] returned = null;

		// sector = (GameSector) world.getSector( 19 );
		// try {
		// sector.decals[ Sector.FACE_N ] = EngineView.loadGraphic(
		// EngineActivity.getInstance().getResources().openRawResource(R.raw.testplate
		// ) );
		// } catch (NotFoundException e1) {
		// // TODO Auto-generated catch block
		// e1.printStackTrace();
		// } catch (IOException e1) {
		// // TODO Auto-generated catch block
		// e1.printStackTrace();
		// }

		for (int c = 0; c < world.getTotalSectors(); ++c) {

			sector = (GameSector) world.getSector(c);

			for (int d = 0; d < 6; ++d) {

				if (sector.decals[d] != null) {

					returned = sector.decals[d];

					for (int e = 0; e < sector.decals[d].length; ++e) {
						sceneGeometryToRender.add(sector.applyToFace(
								Sector.FACE_FLOOR, returned[c]));
					}
				}
			}
		}

		ObjMesh detailMesh = world.getDetailMesh();

		Mesh mesh;
		if (detailMesh != null) {

			for (int c = 0; c < detailMesh.getMeshes().size(); ++c) {

				mesh = detailMesh.getMeshes().get(c);

				if (mesh.getName().equals("basemesh"))
					continue;

				for (int d = 0; d < mesh.getTotalItems(); ++d) {
					this.sceneGeometryToRender.add((GLESIndexedSetFace) mesh
							.getFace(d));
				}
			}

			fixedGeometryManager = new GLESVertexArrayManager();
			fixedGeometryManager.init(sceneGeometryToRender.size());

			GLESIndexedSetFace face;

			for (int c = 0; c < sceneGeometryToRender.size(); ++c) {
				face = sceneGeometryToRender.get(c);
				fixedGeometryManager.pushIntoFrameAsStatic(
						face.getVertexData(), face.getColorData());
			}

			sceneGeometryToRender.clear();
		}
		System.gc();
		Decal decal;
		GameSector s;
		for (int c = 0; c < world.getTotalSectors(); ++c) {
			
			s = (GameSector) world.getSector(c);
			
			if ( !s.isMaster() )
				continue;
			
			for (int d = 0; d < 6; ++d) {
				if (s.getDoor(d) != null && s.getDoor(d).getMesh() instanceof Decal) {
					
					decal = (Decal) s.getDoor(d).getMesh();
					
					if ( !meshes.contains( decal ) )
						meshes.add( decal );
				}
			}
		}
	}

	final private void lit(int originalSector, int candelas) {

		int top = 0;
		// Stack<Integer> sectorsToVisit = new Stack<Integer>();
		// sectorsToVisit.push( originalSector );
		// sectorsToVisit.push( candelas );
		lights[1] = candelas;
		lights[0] = originalSector;
		top = 2;
		int sector;
		int factor;
		GameSector sec;
		// Log.d( "BZK3", "spreading " + candelas + " candelas at " +
		// originalSector );
		while (top > 0) {

			factor = lights[top - 1];
			top--;
			sector = lights[top - 1];
			top--;
			// factor = sectorsToVisit.pop();
			// sector = sectorsToVisit.pop();
			sec = (GameSector) this.world.getSector(sector);

			factor = (factor - sec.visibleFaces) / 2;

			sec.addCandelas((short) factor);

			if (factor <= 0)
				continue;

			for (int c = 0; c < 6; ++c) {
				try {
					if (sec.isOpenAt(c)) {

						// sectorsToVisit.push( sec.getLink( c ) );
						// sectorsToVisit.push( factor );
						lights[top] = sec.getLink(c);
						lights[top + 1] = factor;
						top += 2;

					}
				} catch (InvalidSlotException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}

		}

	}

	// ------------------------------------------------------------------------------------------------------------

	// ------------------------------------------------------------------------------------------------------------
	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {

		Log.d("BZK3", "on surface changed");

		if (height == 0) {
			height = 1;
		}

		gl.glViewport(0, 0, width, height);
		gl.glMatrixMode(GL10.GL_PROJECTION);
		gl.glLoadIdentity();

		GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f,
				255.0f);

		gl.glMatrixMode(GL10.GL_MODELVIEW);
		gl.glLoadIdentity();

	}

	// ------------------------------------------------------------------------------------------------------------
	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		gl.glShadeModel(GL10.GL_FLAT);
		;
		gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		gl.glClearDepthf(1.0f);
		gl.glEnable(GL10.GL_DEPTH_TEST);
		gl.glDepthFunc(GL10.GL_LEQUAL);
		gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);

		// gl.glFrontFace(GL10.GL_CCW); // OpenGL docs
		// gl.glEnable(GL10.GL_CULL_FACE); // OpenGL docs
		// gl.glCullFace(GL10.GL_BACK); // OpenGL docs
	}

	// -------------------------------------------------------------------------------------------------
	@Override
	public void onDrawFrame(GL10 gl) {

		long t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, ta, tb;
		t7 = System.currentTimeMillis();
		int size;

		// if (getWorld() == null
		// || (getWorld() != null && getWorld().getTotalSectors() == 0))
		// return;

		// if ( EngineView.loaded )
		// return;
		// t0 = System.currentTimeMillis();
		calculateLightning();
		// t1 = System.currentTimeMillis();

		GLESIndexedSetFace t;
		++frame;

		// t2 = System.currentTimeMillis();
		if (shouldResetView) {
			// synchronized (this) {
			manager.onFrameRenderingFinished();
			setView();
			shouldResetView = false;
			// }
		}
		// t3 = System.currentTimeMillis();

		gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
		setCamera(gl);
		gl.glEnable(GL10.GL_DEPTH_TEST);
		gl.glFrontFace(GL10.GL_CW);
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
		// t4 = System.currentTimeMillis();

		if (fixedGeometryManager != null) {

			fixedGeometryManager.flushToGLES();
			fixedGeometryManager.draw(gl);
		}
		ObjMesh objMesh;
		GameActor actor;

		for (int z = 1; z < this.actors.size(); ++z) {
			actor = actors.get(z);
			ArrayList<ObjMesh> objMeshes = actor.getMesh();
			ArrayList<Mesh> meshes;
			Mesh mesh;

			gl.glPushMatrix();
			gl.glTranslatef(actor.getPosition().getX(), actor.getPosition()
					.getY(), actor.getPosition().getZ());
			gl.glRotatef(360.0f - actor.getAngleXZ(), 0.0f, 1.0f, 0.0f);

			for (int c = 0; c < objMeshes.size(); ++c) {
				objMesh = objMeshes.get(c);
				drawObjMesh(gl, objMesh);
			}
			gl.glPopMatrix();
		}

		for (int z = 0; z < this.sceneGeometryToRender.size(); ++z) {
			sceneGeometryToRender.get(z).draw(gl);
		}

		for (int z = 0; z < meshes.size(); ++z) {
			drawMesh(gl, meshes.get(z));
		}

		// setCamera( gl );
		manager.flushToGLES();
		manager.draw(gl);

		// t5 = System.currentTimeMillis();
		gl.glDisable(GL10.GL_DEPTH_TEST);
		gl.glLoadIdentity();

		// t6 = System.currentTimeMillis();
		size = screenShapesToRender.size();
		for (int c = 0; c < size; ++c) {
			t = screenShapesToRender.get(c);
			t.draw(gl);
		}
		// t7 = System.currentTimeMillis();
		gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

		// t8 = System.currentTimeMillis();
		// não ativar enquanto estiver usando recalculo seletivo
		// manager.onFrameRenderingFinished();
		// t9 = System.currentTimeMillis();

		// ta = System.currentTimeMillis();
		resetLightning();
		// tb = System.currentTimeMillis();

		// Log.d( "BZK3", "times: " + ( t1 - t0 ) + "," + ( t3 - t2 )+ "," +( t5
		// - t4 )+ "," + ( t7 - t6 )+ "," + ( t9 - t8 )+ "," + ( tb -ta ) );

	}

	/**
	 * @param gl
	 * @param objMesh
	 */
	private void drawObjMesh(GL10 gl, ObjMesh objMesh) {
		
		
		if ( !objMesh.isVisible() )
			return;
		
		ArrayList<Mesh> meshes;
		Mesh mesh;
		meshes = objMesh.getMeshes();
		for (int d = 0; d < meshes.size(); ++d) {
			mesh = meshes.get(d);
			drawMesh(gl, mesh);
		}
	}

	/**
	 * @param gl
	 * @param mesh
	 */
	private void drawMesh(GL10 gl, Mesh mesh) {
		
		if ( !mesh.isVisible() )
			return;
		
		for (int e = 0; e < mesh.faces.size(); ++e) {

			((GLESIndexedSetFace) mesh.faces.get(e)).draw(gl);
		}
		
	//	mesh.setVisibility( false );
	}

	// ------------------------------------------------------------------------------------------------------------
	public void setAngle(float Angle) {
		angle = Angle;
		this.needsToResetView();
	}

	private void setCamera(GL10 gl) {
		gl.glLoadIdentity();
		gl.glRotatef( -7.5f + accel.getX(), 1, 0, 0);
		gl.glRotatef(angle, 0, 1, 0);
		gl.glTranslatef(-this.cameraActor.getPosition().getX(),
				-this.cameraActor.getPosition().getY(), -this.cameraActor
						.getPosition().getZ());
	}

	// ------------------------------------------------------------------------------------------------------------a

	public void setCurrentCamera(GameActor cameraActor) {
		this.cameraActor = cameraActor;
	}

	public void setView() {
		// manager.onFrameRenderingFinished();

		if (cameraActor.getCurrentSector() != Constants.NO_LINK) {
			direction = cameraActor.getDirection();
			parcel = ( ( cameraActor.getDiscreetAngle() ) / 4.0f ) - direction;
			back = Utils.getOppositeDirection(direction);
			visitFirst(cameraActor.getCurrentSector(), direction);
		}
	}

	/**
	 * @param world
	 *            the world to set
	 */
	public void setWorld(GameWorld world) {
		this.world = world;
	}

	// ------------------------------------------------------------------------------------------------------------
	@Override
	public String toString() {
		return displayList;
	}

	final private void visit(int sec, int currentDirection, Vec3 currentPos) {
		if (top < toVisit.length) {

			toVisit[top] = (GameSector) world.getSector(sec);
			toVisit[top].relPos = (currentPos);
			// toVisit[top].relPos.setIndex(currentDirection);
			// incidingVisit[ top ] = currentDirection;
			// relativePositions[ top ].set( currentPos );
			// relativePositions[ top ].addTo( offset );
			++top;
		}
	}

	final private void visitFirst(int sec, int currentDirection) {
		top = 0;

		// offset é a direção ja transformada para o espaço de câmera. Ou seja,
		// pra frente sempre será 0.
		int offset;
		Vec3 currentPos = ((GameSector) world.getSector(sec)).relPos;
		float x = currentPos.getX();
		float y = currentPos.getY();
		float z = currentPos.getZ();
		// visit(sec, dir, currentPos);
		toVisit[top] = (GameSector) world.getSector(sec);
		toVisit[top].relPos = (currentPos);

		GameSector s;
		GameSector original = toVisit[top];
		GLES1Square square;
		++top;
		// if ( original.leafCache[ currentDirection ] != null ) {
		// for ( int d = 0; d < original.leafCache[ currentDirection ].length;
		// ++d ) {
		// s = original.leafCache[ currentDirection ][ d ];
		//
		// for (int c = 0; c < 6; ++c) {
		//
		// if (c != back) {
		//
		// if ( !s.isOpenAt(c)) {
		//
		// square = s.face[c];
		//
		// if (square != null && square.getVisibility())
		// manager.pushIntoFrameAsStatic(
		// square.getVertexData(),
		// square.getColorData());
		// }
		// }
		// }
		// }
		// return;
		// }
		//
		// ArrayList< GameSector > currentLeafCache = new ArrayList< GameSector
		// >();
		while (top > 0) {

			--top;
			s = toVisit[top];

			if (s.frame == frame)
				continue;

			// Z fica menor com maiores distâncias...
			// if ( Math.abs( s.relPos.getX() ) > -s.relPos.getZ() && Math.abs(
			// s.relPos.getX() ) > 1.0f && s.relPos.getZ() < -1.0f )
			// continue;

			s.frame = frame;
			currentPos = s.relPos;
			x = currentPos.getX();
			y = currentPos.getY();
			z = currentPos.getZ();

			if (s.visibleFaces > 0) {
				// currentLeafCache.add( s );
				// s.setVisible(true);
				s.lit();
			}

			// setores que não estão visiveis, mas que vão interferir com a
			// luminosidade da cena no frame seguinte, mesmo que continuem não
			// visiveis, não serão resetados.
			// por isso, não posso fazer isso só aqui.
			// s.setEmissiveLightningIntensity( (short) 0 );

			// try {

			for (int c = 0; c < 6; ++c) {

			if (c != back) {

					if (s.isOpenAt(c)) {
						if (c < 4) {

							offset = (c - currentDirection);
							offset = offset % 4;

							if (offset < 0)
								offset += 4;

							switch (offset) {

							case Sector.FACE_FLOOR:
								if (y > 0)
									continue;
								break;
							case Sector.FACE_CEILING:
								if (y < 0)
									continue;
								break;
							case Sector.FACE_W:
								if (x > 0)
									continue;

								// Z fica menor com maiores distâncias...
								// if ( Math.abs( s.relPos.getX() ) >
								// -s.relPos.getZ() && Math.abs(
								// s.relPos.getX() ) > 1.0f && s.relPos.getZ() <
								// -1.0f )
								// continue;
								break;

							case Sector.FACE_E:
								if (x < 0)
									continue;

								// Z fica menor com maiores distâncias...
								// if ( Math.abs( s.relPos.getX() ) >
								// -s.relPos.getZ() && Math.abs(
								// s.relPos.getX() ) > 1.0f && s.relPos.getZ() <
								// -1.0f )
								// continue;
								break;
							}
						} else {
							offset = c;
						}

						if (top < toVisit.length) {

							toVisit[top] = // (GameSector)
											// world.getSector(s.getLink(c));
							s.cachedNeighBours[c];

							toVisit[top].relPos.set(currentPos);
							toVisit[top].relPos
									.addTo(relativePositions[offset]);
							++top;
						}
					} else {
						if (s.meshWalls[c] == null) {

							if (s.getDoor(c) != null && s.getDoor(c).getMesh() instanceof Decal ) {
								
								if ( !s.getDoor(c).getMesh().isVisible() )
									s.getDoor(c).getMesh().setVisibility(true);
								//continue;
							} else {
								
								square = s.face[c];
								
								// if (square != null && square.getVisibility())
								manager.pushIntoFrameAsStatic(square.vertices,
										square.color);
							}
						}
					}
				}
			}
			// } catch (InvalidSlotException e) {
			// }
		}

		// original.leafCache[currentDirection] = new GameSector[
		// currentLeafCache.size() ];
		//
		// for ( int c = 0; c < original.leafCache[currentDirection].length; ++c
		// )
		// original.leafCache[currentDirection][ c ] = currentLeafCache.get( c
		// );
	}

	public synchronized void needsToResetView() {
		shouldResetView = true;
	}

	public synchronized void NoNeedsToSetView() {
		shouldResetView = false;
	}

	public void addGeometryToScreen(GLES1Triangle[] graphic) {
		for (int c = 0; c < graphic.length; ++c) {
			this.addGeometryToScreen(graphic[c]);
		}
	}
}
